local Span = require('skywalking.span')
local TC = require('skywalking.tracing_context')
local Layer = require('skywalking.span_layer')
local Segment = require('skywalking.segment')
local Util = require("skywalking.util")
local Const = require('skywalking.constants')
local json = require('cjson.safe')

local log_reporter = require('skywalking.reporter.log_reporter')
local SegmentRef = require("skywalking.segment_ref")
local Default_logger = require('skywalking.default_logger')

local ngx = ngx
local table = table
local string = string
local tonumber = tonumber

local Tracer = {}

local default_logger = Default_logger.new()
local default_reporter = log_reporter.new(default_logger)

Tracer.service_name = ''
Tracer.service_instance_name = ''
Tracer.logger = default_logger
Tracer.sampler = nil -- 采样器
Tracer.reporter = default_reporter -- 先设置默认的reporter

--- 初始化tracer
--- @param service_name 服务名
--- @param ... tracer_options
function Tracer:init(service_name, ...)
    service_name = service_name or Const.Default_Service_Name
    local service_instance_name = string.format("%s@%s", Util.GetHostName(), Util.GetLocalIP())
    service_instance_name = service_instance_name or Const.Default_Instance_Name

    Tracer.service_name = service_name
    Tracer.service_instance_name = service_instance_name

    local tracer_options = {...}
    for _, func in pairs(tracer_options) do
        func(Tracer)
    end

    -- set default value
    Tracer.reporter = Tracer.reporter or log_reporter.new(Tracer.logger) 
end

--- start for openresty
--- 业务项目
function Tracer:start(peer_name, correlation)
    local ctx = ngx.ctx
    local req_uri = ngx.var.uri

    local carrier = Util.tablepool_fetch("sw_contextCarrier")
    carrier[Const.HeaderKeySw8] = ngx.var.http_sw8
    carrier[Const.HeaderKeySw8Correlation] = ngx.var.http_sw8_correlation
    local entrySpan = Tracer:createEntrySpan(ctx, req_uri, carrier)
    -- if ngx.is_subrequest then
    --     local host = ngx.var.proxy_host or ngx.var.host
    --     entrySpan = Tracer:createEntrySpan(ctx, host .. req_uri, carrier)
    -- else
    --     entrySpan = Tracer:createEntrySpan(ctx, req_uri, carrier)
    -- end

    entrySpan:setComponentId(entrySpan.ComponentId_Constants.NginxComponentId)
    entrySpan:setLayer(Layer.HTTP)
    entrySpan:tag(entrySpan.Tag_Constants.TagHTTPMethod, ngx.req.get_method())
    entrySpan:tag(entrySpan.Tag_Constants.TagHTTPParams, ngx.var.args or '')

    -- self defined below:
    entrySpan:tag(entrySpan.Tag_Constants.TagXRealIP,            ngx.req.get_headers()["x-real-ip"]          or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagXForwardedFor,      ngx.req.get_headers()["x-forwarded-for"]    or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagXIsClientIP,        ngx.req.get_headers()["x-is-client-ip"]     or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagUserAgent,          ngx.req.get_headers()["user-agent"]         or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagHTTPHost,           ngx.req.get_headers()["host"]               or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagXRequestID,         ngx.req.get_headers()["x-request-id"]       or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagXDeviceID,          ngx.req.get_headers()["x-is-device-id"]     or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagContentLength,      ngx.req.get_headers()["content-length"]     or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagContentType,        ngx.req.get_headers()["content-type"]       or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagXUserID,          ngx.req.get_headers()["x-user-id"]         or "" )

    local injector = ngx.req.set_header
    local exitSpan = Tracer:createExitSpan(ctx, req_uri, peer_name, injector, correlation)

    exitSpan:setComponentId(exitSpan.ComponentId_Constants.NginxComponentId)
    exitSpan:setLayer(Layer.HTTP)

    ctx.entrySpan = entrySpan
    ctx.exitSpan = exitSpan
end

--- start for dbproxsy
function Tracer:dbproxy_start(peer_name, correlation)
    local ctx = ngx.ctx
    local req_uri = ngx.var.uri

    local carrier = Util.tablepool_fetch("sw_contextCarrier")
    carrier[Const.HeaderKeySw8] = ngx.var.http_sw8
    carrier[Const.HeaderKeySw8Correlation] = ngx.var.http_sw8_correlation
    local entrySpan = Tracer:createEntrySpan(ctx, req_uri, carrier)
    -- if ngx.is_subrequest then
    --     local host = ngx.var.proxy_host or ngx.var.host
    --     -- entrySpan = TC.createEntrySpan(tracingContext, host .. req_uri, nil, contextCarrier)
    --     entrySpan = Tracer:createEntrySpan(ctx, host .. req_uri, carrier)
    -- else
    --     entrySpan = Tracer:createEntrySpan(ctx, req_uri, carrier)
    -- end

    entrySpan:setComponentId(entrySpan.ComponentId_Constants.NginxComponentId)
    entrySpan:setLayer(Layer.HTTP)
    entrySpan:tag(entrySpan.Tag_Constants.TagHTTPMethod, ngx.req.get_method())
    entrySpan:tag(entrySpan.Tag_Constants.TagHTTPParams, ngx.var.args or '')

    -- self defined below:
    entrySpan:tag(entrySpan.Tag_Constants.TagXRealIP,            ngx.req.get_headers()["x-real-ip"]          or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagXForwardedFor,      ngx.req.get_headers()["x-forwarded-for"]    or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagHTTPHost,           ngx.req.get_headers()["host"]               or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagXRequestID,         ngx.req.get_headers()["x-request-id"] or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagXUserID,            ngx.req.get_headers()["x-user-id"]         or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagXDeviceID,          ngx.req.get_headers()["x-is-device-id"]     or "" )

    ctx.entrySpan = entrySpan
end

--- start for proxsy(前端代理)
function Tracer:proxy_start(peer_name)
    local ctx = ngx.ctx
    local req_uri = ngx.var.uri

    local carrier = Util.tablepool_fetch("sw_contextCarrier")
    carrier[Const.HeaderKeySw8] = ngx.var.http_sw8
    carrier[Const.HeaderKeySw8Correlation] = ngx.var.http_sw8_correlation
    local entrySpan = Tracer:createEntrySpan(ctx, req_uri, carrier)
    -- if ngx.is_subrequest then
    --     local host = ngx.var.proxy_host or ngx.var.host
    --     entrySpan = Tracer:createEntrySpan(ctx, host .. req_uri, carrier)
    -- else
    --     entrySpan = Tracer:createEntrySpan(ctx, req_uri, carrier)
    -- end

    entrySpan:setComponentId(entrySpan.ComponentId_Constants.NginxComponentId)
    entrySpan:setLayer(Layer.HTTP)
    entrySpan:tag(entrySpan.Tag_Constants.TagHTTPMethod, ngx.req.get_method())
    entrySpan:tag(entrySpan.Tag_Constants.TagHTTPParams, ngx.var.args or '')

    -- self defined below:
    entrySpan:tag(entrySpan.Tag_Constants.TagXRealIP,            ngx.var.remote_addr          or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagUserAgent,          ngx.req.get_headers()["user-agent"]         or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagHTTPHost,           ngx.req.get_headers()["host"]               or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagXRequestID,         ngx.req.get_headers()["x-request-id"]       or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagContentLength,      ngx.req.get_headers()["content-length"]     or "" )
    entrySpan:tag(entrySpan.Tag_Constants.TagContentType,        ngx.req.get_headers()["content-type"]       or "" )

    local injector = ngx.req.set_header
    local exitSpan = Tracer:createExitSpan(ctx, req_uri, peer_name, injector)

    exitSpan:setComponentId(exitSpan.ComponentId_Constants.NginxComponentId)
    exitSpan:setLayer(Layer.HTTP)

    ctx.entrySpan = entrySpan
    ctx.exitSpan = exitSpan
end

--- 获取当前的entrySpan
function Tracer:getActiveEntrySpan()
    return ngx.ctx["entrySpan"]
end

--- @param context
--- @param operationName sapn的操作名称,建议url或者函数名
--- @param carrier map {sw8:"xxxxxxx", "sw8-correlation":"xxxxxx"}
function Tracer:createEntrySpan(context, operationName, carrier)
    local parent_span = context[Const.SegmentSpanKey]

    if parent_span then
        if parent_span:isNoop() then
            return parent_span
        end
    end

    local serviceName = self.service_name or ''
    local serviceInstanceName = self.service_instance_name or ''
    local tracingContext = TC.new(serviceName, serviceInstanceName)

    tracingContext.correlation_context:decode(carrier)
    tracingContext.is_finished = false
    
    local entry_span = Span.new(operationName, tracingContext, parent_span)
    entry_span.is_entry = true

    -- 将上游上传的信息,解码到SegmentRef
    local ref = SegmentRef.decode(carrier)

    if ref ~= nil then
        tracingContext:addRefIfFirst(ref)
        table.insert(entry_span.refs, ref)
    end

    context["skywalking_trace_id"] = tracingContext.trace_id

    if not ref and self.sampler then
        -- 是否可以采样
        local is_sample = self.sampler(tracingContext)
        -- Tracer.logger.info("is_sample:" .. tostring(is_sample))
        if not is_sample then
            tracingContext.sample = 0
        end
    end

    context[Const.TracingContextKey] = tracingContext
    context[Const.SegmentSpanKey] = context[Const.SegmentSpanKey] or entry_span

    entry_span:start(ngx.now()*1000)
    return entry_span, context
end

--- @param context
--- @param operationName sapn的操作名称,建议url或者函数名
--- @param peer_name
--- @param injector 注入器，比如设置http头部的函数, function func(header, value)
--- @param correlation map 需要传输到下一个链路的上下文
function Tracer:createExitSpan(context, operationName, peer_name, injector, correlation)
    local tracing_context = context[Const.TracingContextKey]
    if not tracing_context then
        return Span.newNooP()
    end

    local parent_span = context[Const.SegmentSpanKey]
    local exit_span = Span.new(operationName, tracing_context, parent_span)
    exit_span.peer = peer_name
    exit_span.is_exit = true

    local injectableRef = SegmentRef.new()
    injectableRef.trace_id = tracing_context.trace_id
    injectableRef.segment_id = tracing_context.segment_id
    injectableRef.span_id = exit_span.span_id
    injectableRef.address_used_at_client = peer_name
    injectableRef.parent_service = tracing_context.service
    injectableRef.parent_service_instance = tracing_context.service_instance
    injectableRef.sample = tracing_context.sample

    if correlation and next(correlation) then
        for k, v in pairs(correlation) do
            tracing_context.correlation_context:put(k, v)
        end
    end

    exit_span:start(ngx.now() * 1000)

    -- 将correlation上下文编码到injector，对应sw8-correlation头部
    tracing_context.correlation_context:encode(injector)

    -- 将上下文编码到injector，对应sw8头部
    if tracing_context.sample == 1 then
        injectableRef:encode(injector)
    end

    return exit_span, context
end

--- @param context
--- @param operationName sapn的操作名称,建议url或者函数名
--- @param peer_name
function Tracer:createLocalSpan(context, operationName, peer_name)
    local tracing_context = context[Const.TracingContextKey]
    if not tracing_context then
        return Span.newNooP()
    end
    
    local parent_span = context[Const.SegmentSpanKey] 
    local local_span = Span.new(operationName, tracing_context, parent_span)
    local_span.peer = peer_name
    local_span:start(ngx.now() * 1000)
    return local_span
end

function Tracer:finish()
    -- Finish the exit span when received the first response package from upstream
    local context = ngx.ctx
    local tracing_context = context[Const.TracingContextKey]
    local exitSpan = context["exitSpan"]

    if exitSpan and tracing_context and not tracing_context.is_finished then
        local upstream_status = tonumber(ngx.var.upstream_status)
        if upstream_status then
            exitSpan:tag(exitSpan.Tag_Constants.TagHTTPStatus, upstream_status)
        end

        exitSpan:finish(ngx.now() * 1000)
        context.exitSpan = nil
    end
end

function Tracer:prepareForReport()
    local context = ngx.ctx
    local tracing_context = context[Const.TracingContextKey]
    if tracing_context and not tracing_context:finished() then
        Tracer.finish()
    end

    local entrySpan = context["entrySpan"]
    if not entrySpan then
        return
    end

    local ngxstatus = ngx.var.status
    entrySpan:tag(entrySpan.Tag_Constants.TagHTTPStatus, ngxstatus)
    if tonumber(ngxstatus) >= 500 then
        entrySpan:errorOccurred()
    end

    entrySpan:finish(ngx.now() * 1000)

    tracing_context:Report(Tracer)

    Util.tablepool_release()
end

return Tracer
